﻿using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Wolverine.Runtime;
using Wolverine.Runtime.RemoteInvocation;

namespace Wolverine.Tracking;

public class TrackedSessionConfiguration
{
    internal TrackedSessionConfiguration(TrackedSession session)
    {
        Session = session;
    }

    internal TrackedSession Session { get; }

    /// <summary>
    ///     Override the default timeout threshold to wait for all
    ///     activity to finish
    /// </summary>
    /// <param name="timeout"></param>
    /// <returns></returns>
    public TrackedSessionConfiguration Timeout(TimeSpan timeout)
    {
        Session.Timeout = timeout;
        return this;
    }

    /// <summary>
    /// Do not track any messages of this type
    /// Helpful for polling operations that maybe happening during your testing
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public TrackedSessionConfiguration IgnoreMessageType<T>()
    {
        return IgnoreMessagesMatchingType(t => t == typeof(T));
    }

    /// <summary>
    /// Register an action that gets called before the actual execution
    /// Typically meant for clean up work
    /// </summary>
    /// <param name="before"></param>
    /// <returns></returns>
    public TrackedSessionConfiguration BeforeExecution(Func<IWolverineRuntime, CancellationToken, Task> before)
    {
        Session.Befores.Add(before);
        return this;
    }

    /// <summary>
    /// Add a secondary stage of the tracked session that will execute like a nested tracked session
    /// </summary>
    /// <param name="stage"></param>
    /// <returns></returns>
    public TrackedSessionConfiguration AddStage(Func<IWolverineRuntime, IMessageContext, CancellationToken, Task> stage)
    {
        Session.SecondaryStages.Enqueue(new TrackedSession.SecondaryStage(Session, stage));
        return this;
    }
    
    /// <summary>
    /// Add a secondary action against the current Wolverine application *after* the tracked session
    /// has completed
    /// </summary>
    /// <param name="stage"></param>
    /// <returns></returns>
    public TrackedSessionConfiguration AfterExecution(Func<IWolverineRuntime, CancellationToken, Task> func)
    {
        Session.SecondaryStages.Enqueue(new TrackedSession.SecondaryAction(Session, func));
        return this;
    }

    /// <summary>
    /// Do not track any messages where the message type matches this filter.
    /// Helpful for polling operations that maybe happening during your testing
    /// </summary>
    /// <param name="filter"></param>
    /// <returns></returns>
    public TrackedSessionConfiguration IgnoreMessagesMatchingType(Func<Type, bool> filter)  
    {
        Session.IgnoreMessageTypes(filter);
        return this;
    }

    /// <summary>
    ///     Track activity across an additional Wolverine application
    /// </summary>
    /// <param name="host"></param>
    /// <returns></returns>
    public TrackedSessionConfiguration AlsoTrack(params IHost?[] hosts)
    {
        // It's actually important to ignore null here
        foreach (var host in hosts)
        {
            if (host != null)
            {
                Session.WatchOther(host);
            }
        }

        return this;
    }
    
    /// <summary>
    ///     Track activity across an additional Wolverine application if you happen to be bootstrapping
    /// the IoC container for testing harnesses outside of IHost usage
    /// </summary>
    /// <param name="host"></param>
    /// <returns></returns>
    public TrackedSessionConfiguration AlsoTrack(params IServiceProvider?[] serviceProviders)
    {
        // It's actually important to ignore null here
        foreach (var serviceProvider in serviceProviders)
        {
            if (serviceProvider != null)
            {
                Session.WatchOther(serviceProvider);
            }
        }

        return this;
    }

    /// <summary>
    ///     Force the message tracking to include outgoing activity to
    ///     external transports
    /// </summary>
    /// <returns></returns>
    public TrackedSessionConfiguration IncludeExternalTransports()
    {
        Session.AlwaysTrackExternalTransports = true;
        return this;
    }

    /// <summary>
    ///     Do not assert or fail if exceptions where thrown during the
    ///     message activity. This is useful for testing resiliency features
    ///     and exception handling with message failures
    /// </summary>
    /// <returns></returns>
    public TrackedSessionConfiguration DoNotAssertOnExceptionsDetected()
    {
        Session.AssertNoExceptions = false;
        return this;
    }

    /// <summary>
    /// Do not assert or fail if failure acks were sent during the message activity.
    /// This might be useful if using request/reply mechanics where you are testing
    /// cases where you want to see if failure acks are sent. Maybe only useful
    /// inside of Wolverine's own testing to be honest:-)
    /// </summary>
    /// <returns></returns>
    public TrackedSessionConfiguration IgnoreFailureAcks()
    {
        Session.AssertAnyFailureAcknowledgements = false;
        return this;
    }

    /// <summary>
    ///     Continue tracking until an expected message is received at this host
    /// </summary>
    /// <param name="host"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public TrackedSessionConfiguration WaitForMessageToBeReceivedAt<T>(IHost host)
    {
        var condition = new WaitForMessage<T>
        {
            UniqueNodeId = host.Services.GetRequiredService<IWolverineRuntime>().Options.UniqueNodeId
        };

        Session.AddCondition(condition);

        return this;
    }

    public TrackedSessionConfiguration WaitForCondition(ITrackedCondition condition)
    {
        Session.AddCondition(condition);
        return this;
    }

    /// <summary>
    ///     Execute a user defined Lambda against an IMessageContext
    ///     and wait for all activity to end
    /// </summary>
    /// <param name="action"></param>
    /// <returns></returns>
    public async Task<ITrackedSession> ExecuteAndWaitAsync(Func<IMessageContext, Task> action)
    {
        Session.Execution = action;
        await Session.ExecuteAndTrackAsync();
        return Session;
    }

    /// <summary>
    ///     Execute a user defined Lambda against an IMessageContext
    ///     and wait for all activity to end
    /// </summary>
    /// <param name="action"></param>
    /// <returns></returns>
    public async Task<ITrackedSession> ExecuteAndWaitAsync(Func<IMessageContext, ValueTask> action)
    {
        Session.Execution = c => action(c).AsTask();
        await Session.ExecuteAndTrackAsync();
        return Session;
    }

    /// <summary>
    ///     Invoke a message inline from the current Wolverine application
    ///     and wait for all cascading activity to complete
    /// </summary>
    /// <param name="message"></param>
    /// <returns></returns>
    public Task<ITrackedSession> InvokeMessageAndWaitAsync(object message)
    {
        return ExecuteAndWaitAsync(c => c.InvokeAsync(message));
    }

    /// <summary>
    ///     Send a message from the current Wolverine application and wait for
    ///     all cascading activity to complete
    /// </summary>
    /// <param name="message"></param>
    /// <returns></returns>
    public Task<ITrackedSession> SendMessageAndWaitAsync(object message, DeliveryOptions? options = null)
    {
        return ExecuteAndWaitAsync(c => c.SendAsync(message, options));
    }

    /// <summary>
    ///     Send a message from the current Wolverine application to a specified topic name and wait for
    ///     all cascading activity to complete
    /// </summary>
    /// <param name="message"></param>
    /// <returns></returns>
    public Task<ITrackedSession> BroadcastMessageToTopicAndWaitAsync(string topicName, object message,
        DeliveryOptions? options = null)
    {
        return ExecuteAndWaitAsync(c => c.BroadcastToTopicAsync(topicName, message, options));
    }

    /// <summary>
    ///     Send a message from the current Wolverine application and wait for
    ///     all cascading activity to complete
    /// </summary>
    /// <param name="destination"></param>
    /// <param name="message"></param>
    /// <param name="options"></param>
    /// <returns></returns>
    public Task<ITrackedSession> SendMessageAndWaitAsync(Uri destination, object message,
        DeliveryOptions? options = null)
    {
        return ExecuteAndWaitAsync(c => c.EndpointFor(destination).SendAsync(message, options));
    }

    /// <summary>
    ///     Publish a message from the current Wolverine application and wait for
    ///     all cascading activity to complete
    /// </summary>
    /// <param name="message"></param>
    /// <returns></returns>
    public Task<ITrackedSession> PublishMessageAndWaitAsync(object? message, DeliveryOptions? options = null)
    {
        return ExecuteAndWaitAsync(c => c.PublishAsync(message, options));
    }

    /// <summary>
    ///     Execute a request with expected reply
    /// </summary>
    /// <param name="requestInvocation"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public async Task<(ITrackedSession, T?)> InvokeAndWaitAsync<T>(object request, TimeSpan? timeout = null)
    {
        T? response = default;

        Func<IMessageContext, Task> invocation = async c => { response = await c.InvokeAsync<T>(request, timeout:timeout); };

        var session = await ExecuteAndWaitAsync(invocation);

        return (session, response);
    }

    /// <summary>
    ///     Execute a request with expected reply
    /// </summary>
    /// <param name="requestInvocation"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public async Task<(ITrackedSession, T?)> InvokeAndWaitAsync<T>(object request, Uri address) where T : class
    {
        T? response = default;

        Func<IMessageContext, Task> invocation = async c =>
        {
            response = await c.EndpointFor(address).InvokeAsync<T>(request, timeout: Session.Timeout);
        };

        var session = await ExecuteAndWaitAsync(invocation);

        return (session, response);
    }

    /// <summary>
    ///     Execute a send and wait operation
    /// </summary>
    /// <param name="sendAndWaitInvocation"></param>
    /// <returns></returns>
    public async Task<(ITrackedSession, Acknowledgement?)> SendMessageAndWaitForAcknowledgementAsync(
        Func<IMessageContext, Task<Acknowledgement>> sendAndWaitInvocation)
    {
        Acknowledgement? response = default;

        Func<IMessageContext, Task> invocation = async c => { response = await sendAndWaitInvocation(c); };

        var session = await ExecuteAndWaitAsync(invocation);

        return (session, response);
    }
}